package terminal

import (
	"context"
	"device-admin/internal/app/grpc/protoc/devicec"
	"device-admin/internal/dal"
	"device-admin/internal/dao"
	"device-admin/internal/models"
	"strings"
	"time"

	"192.168.1.75/go-pkg/errorx"
	"192.168.1.75/go-pkg/helper"
	"192.168.1.75/go-pkg/logx"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"google.golang.org/protobuf/proto"
)

// NewTerminalCli
func NewTerminalCli(robotID, terminalType, sessionID string) (devicec.DeviceService_MsgStreamWebMsgProxyClient, *grpc.ClientConn, error) {
	// 设备id不能为空
	if robotID == "" {
		return nil, nil, errorx.New("设备id不能为空", -1)
	}
	// 获取sessionID失败
	if sessionID == "" {
		sessionID = helper.RandString(32)
	}
	// 获取终端所在的服务ip,建立grpc连接，以进行数据中转
	cli, conn, err := SessionServerGrpcClient(robotID, "webshell95")
	if err != nil {
		return nil, nil, errorx.New(err.Error(), -1)
	}
	// metadata
	mdContext := metadata.AppendToOutgoingContext(
		context.Background(),
		"clientid", robotID, // 节点id
		"terminal_type", terminalType, // 终端类型
		"session_id", sessionID, // 会话id
	)
	// webshell cli
	webshellCli, err := cli.MsgStreamWebMsgProxy(mdContext)
	if err != nil {
		return nil, conn, errorx.New("连接数据终端代理失败", -1)
	}
	// 新建会话
	{
		msg := &devicec.Msg{}
		cmd := &devicec.NewWebShellSession{}
		content, _ := proto.Marshal(cmd)
		msg.Name = string(cmd.ProtoReflect().Descriptor().FullName())
		msg.Content = content
		msg.SessionId = sessionID
		webshellCli.Send(msg)
	}
	return webshellCli, conn, nil
}

// SendShellCMDAndRecvRes 执行一条webshell指令并获取结果
// 命令发出后,如
// ~ $ pwd        ~$ 为命令提示符, 实际使用中可能为其它
// ~ $ ./
// ~ $  // 在实际的数据返回时，会多一个 ~ $ 提示符
// 所以整个逻辑是，在命令发出后才开始接收，并去除后面的 ~ $
func SendShellCMDAndRecvRes(robotID, cmd string, timeout int) (string, error) {
	if !strings.HasSuffix(cmd, "\n") {
		cmd = cmd + "\n"
	}
	if timeout <= 0 || timeout >= 30 {
		timeout = 10
	}
	cli, conn, err := NewTerminalCli(robotID, "webshell95", helper.RandString(32))
	if conn != nil {
		defer conn.Close()
	}
	if err != nil {
		return "", errorx.New("连接失败", -1)
	}
	// 发送获取文件信息信号
	sendMsgChan := make(chan struct{}, 1)
	sendMsgChan <- struct{}{}
	shellcmd := &devicec.WebShellCMD{}
	shellcmd.Data = []byte(cmd)
	sendMsg := &devicec.Msg{}
	sendMsg.Name = string(shellcmd.ProtoReflect().Descriptor().FullName())
	sendMsg.Content, _ = proto.Marshal(shellcmd)

	// 等待文件信息回复，超时时间为5s
	var shellRes []string // 用来接收执行结果
	var enableRecv bool
	linesStr, lineStr := []string{}, ""
	// 返回的首行是输入的命令，第二行才是结果
	var lcount = 0
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout))
	defer cancel()
	for {
		select {
		case <-ctx.Done():
			// 获取命令起始标记符
			cmdStartFlagStr := ""
			if len(linesStr) > 0 {
				linesStr = append(linesStr, lineStr) // 追加最后一行
				cmdStartFlagStr = linesStr[len(linesStr)-1]
			}
			for _, str := range linesStr {
				if !strings.Contains(str, cmdStartFlagStr) {
					shellRes = append(shellRes, str)
				}
			}
			return strings.Join(shellRes, "\n"), nil
		case <-sendMsgChan:
			time.AfterFunc(time.Second*2, func() { // 等待teleport agent准备完成
				cli.Send(sendMsg)
				enableRecv = true
			})
		default:
			msg, err := cli.Recv()
			if err != nil {
				return strings.Join(shellRes, ""), err
			}
			shellcmd := &devicec.WebShellCMD{}
			proto.Unmarshal(msg.Content, shellcmd)
			validStr := "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~!@#$%^&*()_+{}|:\"<>?`-=[]\\;',./\x20\t"
			// 命令发出后，才开始接收
			if enableRecv {
				for _, b := range shellcmd.GetData() {
					if strings.Contains(validStr, string(b)) {
						if lcount >= 1 { // 首行是发出的命令回显行，从第二行才是接首行
							lineStr += string(b)
						}
					} else if b == []byte("\n")[0] {
						if lineStr != "" { // 首次换行时，lineStr为空，需舍弃，否则多一行空
							linesStr = append(linesStr, lineStr)
						}
						lineStr = ""
						lcount++
					}
				}
			}
		}
	}
}

// SendAndReceiveTerminalMsg 发送并接受Msg
func SendAndReceiveTerminalMsg(
	webshellCli devicec.DeviceService_MsgStreamWebMsgProxyClient,
	sendMsg *devicec.Msg,
	receiveMsgName string,
) (receiveMsg *devicec.Msg, err error) {
	// 发送获取文件信息信号
	sendMsgChan := make(chan struct{}, 1)
	sendMsgChan <- struct{}{}
	// 等待文件信息回复，超时时间为5s
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()
	for {
		select {
		case <-ctx.Done():
			return nil, errorx.New("获取文件信息超时", -1)
		case <-sendMsgChan:
			webshellCli.Send(sendMsg)
		default:
			msg, err := webshellCli.Recv()
			if err != nil {
				return nil, errorx.New("获取文件信息失败", -1)
			}
			if receiveMsgName != "" && receiveMsgName == msg.Name {
				return msg, nil
			}
		}
	}
}

// GetTerminalFileInfo 获取终端文件信息
func GetTerminalFileInfo(robotID, terminalType, fileURL string) (fileSize int64, err error) {
	fileInfoReq := &devicec.FileInfoReq{}
	fileInfoReq.FileUrl = fileURL
	content, _ := proto.Marshal(fileInfoReq)
	msg := &devicec.Msg{}
	msg.Name = string(fileInfoReq.ProtoReflect().Descriptor().FullName())
	msg.Content = content
	receivedMsg := &devicec.Msg{}
	receivedMsgName := string((&devicec.FileInfoRsp{}).ProtoReflect().Descriptor().FullName())
	sessionID := helper.RandString(32)
	cli, conn, err := NewTerminalCli(robotID, terminalType, sessionID)
	if conn != nil {
		defer conn.Close()
	}
	if err != nil {
		return 0, errorx.New("获取文件信息失败", -1)
	}
	receivedMsg, err = SendAndReceiveTerminalMsg(cli, msg, receivedMsgName)
	if err != nil {
		return 0, err
	}
	fileInfoRsp := &devicec.FileInfoRsp{}
	err = proto.Unmarshal(receivedMsg.Content, fileInfoRsp)
	if err != nil {
		return 0, errorx.New("获取文件信息失败", -1)
	}
	if fileInfoRsp.Code != 0 {
		return 0, errorx.New("检查文件是否存在", -1)
	}
	return fileInfoRsp.FileSize, nil
}

// GetTerminalFileContent 获取终端文件内容
func GetTerminalFileContent(robotID, terminalType, fileURL string, offset, length int64) (fileContent []byte, err error) {
	fileContentReq := &devicec.FileContentReq{}
	fileContentReq.FileUrl = fileURL
	fileContentReq.Offset = offset
	fileContentReq.Length = length
	content, _ := proto.Marshal(fileContentReq)
	msg := &devicec.Msg{}
	msg.Name = string(fileContentReq.ProtoReflect().Descriptor().FullName())
	msg.Content = content
	receivedMsg := &devicec.Msg{}
	receivedMsgName := string((&devicec.FileContentRsp{}).ProtoReflect().Descriptor().FullName())
	sessionID := helper.RandString(32)
	cli, conn, err := NewTerminalCli(robotID, terminalType, sessionID)
	if conn != nil {
		defer conn.Close()
	}
	if err != nil {
		return nil, err
	}
	receivedMsg, err = SendAndReceiveTerminalMsg(cli, msg, receivedMsgName)
	if err != nil {
		return nil, err
	}
	fileContentRsp := &devicec.FileContentRsp{}
	err = proto.Unmarshal(receivedMsg.Content, fileContentRsp)
	if err != nil {
		return nil, errorx.New("")
	}
	return fileContentRsp.Content, nil
}

// ScreenCap 97/98 屏幕截图
func Screencap(ctx context.Context, robotID string, terminalType string, userID string) (url string, err error) {
	spanCtx := logx.Start(ctx, "Screencap")
	defer logx.End(spanCtx)

	if terminalType == "" {
		return "", errorx.New("终端类型不能为空", -1)
	}

	sessionID := helper.RandString(32)
	// 获取终端所在的服务ip,建立grpc连接，以进行数据中转
	cli, grpcConn, err := SessionServerGrpcClient(robotID, "webshell95")
	// 命令执行完成后，关闭连接
	if grpcConn != nil {
		defer grpcConn.Close()
	}
	if err != nil {
		logx.Error(spanCtx, "screenShot get grpcConn failed", logx.String("robot_id", robotID), logx.Err(err))
		return "", errorx.New("截图失败", -1)
	}
	// webshell cli
	mdContext := metadata.AppendToOutgoingContext(
		context.Background(),
		"clientid", robotID, // 节点id
		"terminal_type", "webshell95", // 终端类型
		"session_id", sessionID, // 会话id
	)
	webshellCli, err := cli.MsgStreamWebMsgProxy(mdContext)
	if err != nil {
		logx.Error(spanCtx, "screenShot connect webmsg proxy failed", logx.String("robot_id", robotID), logx.Err(err))
		return "", errorx.New("截图失败", -1)
	}
	// 新建会话
	msg := &devicec.Msg{}
	cmd := &devicec.NewWebShellSession{}
	cmd.InitCmd = []string{
		"export LC_ALL=en_US.UTF-8", // 设置编码
	}
	content, _ := proto.Marshal(cmd)
	msg.Name = string(cmd.ProtoReflect().Descriptor().FullName())
	msg.Content = content
	msg.SessionId = sessionID
	webshellCli.Send(msg)
	screencapName := time.Now().Format("2006-01-02_15:04:05") + ".png"
	// 发送命名
	var cmds = []string{
		"mkdir ./screencap" + terminalType + " \n",                                  // 创建目录
		"find ./screencap" + terminalType + " -type f -mtime +3 -exec rm {} \\; \n", // 清除过期的截图,保存3天
		"adb disconnect  \n",                                                     // 断开所有的adb连接
		"adb connect 192.168.0." + terminalType + " \n",                          // 连接97
		"cd ./screencap" + terminalType + " \n",                                  // 进入目录
		"adb exec-out screencap -p > ./" + screencapName + " \n",                 // 执行截图
		"convert -sample 20%x20% " + screencapName + " " + screencapName + " \n", // 减小图片尺寸为原来的一般
		"adb disconnect  \n",                                                     // 释放adb连接
	}
	// 下发命令
	for _, sendData := range cmds {
		msg := &devicec.Msg{}
		cmd := &devicec.WebShellCMD{}
		cmd.Data = []byte(sendData)
		content, _ := proto.Marshal(cmd)
		msg.Name = string(cmd.ProtoReflect().Descriptor().FullName())
		msg.Content = content
		msg.SessionId = sessionID
		webshellCli.Send(msg)
	}
	// 等待完成
	time.Sleep(time.Second * 3)
	// 判断截图是否成功
	screencapPath := "./screencap" + terminalType + "/" + screencapName
	fileSize, err := GetTerminalFileInfo(robotID, "webshell95", screencapPath)
	// 如果截图失败，再等待2s
	if fileSize <= 0 || err != nil {
		time.Sleep(time.Second * 2)
		fileSize, err = GetTerminalFileInfo(robotID, "webshell95", screencapPath)
	}
	// 截图成功，保存截图记录
	if err == nil && fileSize > 0 {
		// 添加截图记录
		dao.TerminalScreencapLog{}.Add(
			spanCtx,
			dal.Q,
			robotID,
			terminalType,
			"/screencap"+terminalType+"/"+screencapName,
			userID,
		)
		// 返回结果
		return "/device-admin/be-file/screencap/" + robotID + "/screencap" + terminalType + "/" + screencapName, nil
	} else {
		// 尝试重启adb server
		SendShellCMDTo95(robotID, []string{
			"echo dadaozhichuang | sudo -S bash -c 'pgrep adbd | xargs kill -9'",
			"echo dadaozhichuang | sudo -S adb start-server",
		}, 10)
		return "", errorx.New("截图失败", -1)
	}
}

// ScreencapList 截图列表
func ScreencapList(
	ctx context.Context,
	robotID, terminalType string,
	page, limit int,
) (list []*models.TerminalScreencapLog, total int64, err error) {
	list, total, err = dao.TerminalScreencapLog{}.List(ctx, dal.Q, robotID, terminalType, page, limit)
	if err != nil {
		return []*models.TerminalScreencapLog{}, 0, err
	}
	return list, total, nil
}

// GetScreecap 获取屏幕截图
func ScreencapPic(ctx context.Context, robotID, screencapName string) (picContent []byte, err error) {
	content, err := GetTerminalFileContent(
		robotID,
		"webshell95",
		screencapName,
		0,
		4000*1024,
	)
	return content, err
}

// SendShellCMDTo95 向95下发shell命令
func SendShellCMDTo95(robotID string, shellCMDs []string, timeout int64) error {
	go func() {
		sessionID := helper.RandString(32)
		// 获取终端所在的服务ip,建立grpc连接，以进行数据中转
		cli, grpcConn, err := SessionServerGrpcClient(robotID, "webshell95")
		// 命令执行完成后，关闭连接
		if grpcConn != nil {
			defer grpcConn.Close()
		}
		if err != nil {
			logx.Error(context.Background(), "create service connection client failed", logx.String("robot_id", robotID), logx.Err(err))
			return
		}
		// webshell cli
		mdContext := metadata.AppendToOutgoingContext(
			context.Background(),
			"clientid", robotID, // 节点id
			"terminal_type", "webshell95", // 终端类型
			"session_id", sessionID, // 会话id
		)
		webshellCli, err := cli.MsgStreamWebMsgProxy(mdContext)
		if err != nil {
			logx.Error(context.Background(), "create grpc client failed", logx.String("robot_id", robotID), logx.Err(err))
			return
		}
		// 新建会话
		msg := &devicec.Msg{}
		cmd := &devicec.NewWebShellSession{}
		cmd.InitCmd = []string{
			"export LC_ALL=en_US.UTF-8", // 设置编码
		}
		content, _ := proto.Marshal(cmd)
		msg.Name = string(cmd.ProtoReflect().Descriptor().FullName())
		msg.Content = content
		msg.SessionId = sessionID
		webshellCli.Send(msg)
		// 下发命令
		for _, sendData := range shellCMDs {
			msg := &devicec.Msg{}
			cmd := &devicec.WebShellCMD{}
			cmd.Data = []byte(sendData + "\n") // 注意加上换行符
			content, _ := proto.Marshal(cmd)
			msg.Name = string(cmd.ProtoReflect().Descriptor().FullName())
			msg.Content = content
			msg.SessionId = sessionID
			webshellCli.Send(msg)
		}
		// 等待超时结束
		ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout))
		defer cancel()
		for {
			select {
			case <-ctx.Done():
				return
			default:
				msg, err := webshellCli.Recv()
				if err != nil {
					return
				}
				// 心跳回复，用来包活会话
				switch msg.Name {
				// ping回复
				case string((&devicec.Ping{}).ProtoReflect().Descriptor().FullName()):
					msg.Ack = 1
					webshellCli.Send(msg)
				}
			}
		}
	}()
	return nil
}
